/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen;

import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.Raster;
import java.awt.image.RenderedImage;
import java.awt.image.WritableRaster;
import java.util.Map;

/**
 * An abstract base class for image operators that require only a fixed rectangular source region around a source pixel
 * in order to compute each destination pixel.
 *
 * <p>The source and the destination images will occupy the same region of the plane. A given destination pixel (x, y)
 * may be computed from the neighborhood of source pixels beginning at (x - leftPadding, y - topPadding) and extending
 * to (x + rightPadding, y + bottomPadding) inclusive.
 *
 * <p>Since this operator needs a region around the source pixel in order to compute the destination pixel, the border
 * destination pixels cannot be computed without any source extension. The source extension can be specified by
 * supplying a BorderExtender that will define the pixel values of the source outside the actual source area.
 *
 * <p>If no extension is specified, the destination samples that cannot be computed will be written in the destination
 * as zero. If the source image begins at pixel (minX, minY) and has width w and height h, the result of performing an
 * area operation will be an image beginning at minX, minY, and having a width of w and a height of h, with the area
 * being computed and written starting at (minX + leftPadding, minY + topPadding) and having width Math.max(w -
 * leftPadding - rightPadding, 0) and height Math.max(h - topPadding - bottomPadding, 0).
 *
 * <p>A <code>RenderingHints</code> for <code>JAI.KEY_REPLACE_INDEX_COLOR_MODEL</code> with the value of <code>
 * Boolean.TRUE</code> will automatically be added to the given <code>configuration</code> and passed up to the
 * superclass constructor so that area operations are performed on the pixel values instead of being performed on the
 * indices into the color map for those operations whose source(s) have an <code>IndexColorModel</code>. This addition
 * will only take place if a value for the <code>JAI.KEY_REPLACE_INDEX_COLOR_MODEL</code> has not already been provided
 * by the user. Note that the <code>configuration</code> Map is cloned before the new hint is added to it. Regarding the
 * value for the <code>JAI.KEY_REPLACE_INDEX_COLOR_MODEL</code> <code>RenderingHints</code>, the operator itself can be
 * smart based on the parameters, i.e. while the default value for the <code>JAI.KEY_REPLACE_INDEX_COLOR_MODEL</code> is
 * <code>Boolean.TRUE</code> for operations that extend this class, in some cases the operator could set the default.
 *
 * @see BorderExtender
 */
public abstract class AreaOpImage extends OpImage {
    /** The number of source pixels needed to the left of the central pixel. */
    protected int leftPadding;

    /** The number of source pixels needed to the right of the central pixel. */
    protected int rightPadding;

    /** The number of source pixels needed above the central pixel. */
    protected int topPadding;

    /** The number of source pixels needed below the central pixel. */
    protected int bottomPadding;

    /** The BorderExtender, may be null. */
    protected BorderExtender extender = null;

    private Rectangle theDest;

    /** Verify that a specified bounds overlaps that of the source image. */
    private static ImageLayout layoutHelper(ImageLayout layout, RenderedImage source) {
        // If at least one of the bounds variables is set then
        // check the overlap with the source image.
        if (layout != null
                && source != null
                && (layout.getValidMask()
                                & (ImageLayout.MIN_X_MASK
                                        | ImageLayout.MIN_Y_MASK
                                        | ImageLayout.WIDTH_MASK
                                        | ImageLayout.HEIGHT_MASK))
                        != 0) {
            // Get the source bounds.
            Rectangle sourceRect = new Rectangle(
                    source.getMinX(), source.getMinY(),
                    source.getWidth(), source.getHeight());

            // Create destination bounds defaulting un-set variables to
            // their respective source values as is done in the superclass
            // OpImage constructor.
            Rectangle dstRect = new Rectangle(
                    layout.getMinX(source), layout.getMinY(source), layout.getWidth(source), layout.getHeight(source));

            // Check for empty intersection.
            if (dstRect.intersection(sourceRect).isEmpty()) {
                throw new IllegalArgumentException(JaiI18N.getString("AreaOpImage0"));
            }
        }

        return layout;
    }

    private static Map configHelper(Map configuration) {

        Map config;

        if (configuration == null) {
            config = new RenderingHints(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.TRUE);
        } else {

            config = configuration;

            // If the user specified a value for this hint, we don't
            // want to change that
            if (!config.containsKey(JAI.KEY_REPLACE_INDEX_COLOR_MODEL)) {
                RenderingHints hints = new RenderingHints(null);
                // This is effectively a clone of configuration
                hints.putAll(configuration);
                config = hints;
                config.put(JAI.KEY_REPLACE_INDEX_COLOR_MODEL, Boolean.TRUE);
            }
        }

        return config;
    }

    /**
     * Constructs an <code>AreaOpImage</code>. The layout variables are set in the standard way by the <code>OpImage
     * </code> constructor.
     *
     * <p>Additional control over the image bounds, tile grid layout, <code>SampleModel</code>, and <code>ColorModel
     * </code> may be obtained by specifying an <code>ImageLayout</code> parameter. If the image bounds are specified
     * but do not overlap the source bounds then an <code>IllegalArgumentException</code> will be thrown. This parameter
     * will be passed to the superclass constructor unchanged.
     *
     * <p>A <code>RenderingHints</code> for <code>JAI.KEY_REPLACE_INDEX_COLOR_MODEL</code> with the value of <code>
     * Boolean.TRUE</code> will automatically be added to the given <code>configuration</code> and passed up to the
     * superclass constructor so that area operations are performed on the pixel values instead of being performed on
     * the indices into the color map for those operations whose source(s) have an <code>IndexColorModel</code>. This
     * addition will only take place if a value for the <code>JAI.KEY_REPLACE_INDEX_COLOR_MODEL</code> has not already
     * been provided by the user. Note that the <code>configuration</code> Map is cloned before the new hint is added to
     * it.
     *
     * @param source A <code>RenderedImage</code>.
     * @param layout An <code>ImageLayout</code> containing the source dimensions before padding, and optionally
     *     containing the tile grid layout, <code>SampleModel</code>, and <code>ColorModel</code>.
     * @param configuration Configurable attributes of the image including configuration variables indexed by <code>
     *     RenderingHints.Key</code>s and image properties indexed by <code>String</code>s or <code>CaselessStringKey
     *     </code>s. This is simply forwarded to the superclass constructor.
     * @param cobbleSources A <code>boolean</code> indicating whether <code>computeRect()</code> expects contiguous
     *     sources.
     * @param extender A BorderExtender, or null.
     * @param leftPadding The desired left padding.
     * @param rightPadding The desired right padding.
     * @param topPadding The desired top padding.
     * @param bottomPadding The desired bottom padding.
     * @throws IllegalArgumentException if <code>source</code> is <code>null</code>.
     * @throws IllegalArgumentException if the user-specified bounds do intersect the source bounds.
     * @since JAI 1.1
     */
    public AreaOpImage(
            RenderedImage source,
            ImageLayout layout,
            Map configuration,
            boolean cobbleSources,
            BorderExtender extender,
            int leftPadding,
            int rightPadding,
            int topPadding,
            int bottomPadding) {
        super(
                vectorize(source), // vectorize() checks for null source.
                layoutHelper(layout, source),
                configHelper(configuration),
                cobbleSources);

        this.extender = extender;
        this.leftPadding = leftPadding;
        this.rightPadding = rightPadding;
        this.topPadding = topPadding;
        this.bottomPadding = bottomPadding;

        if (extender == null) {

            int d_x0 = getMinX() + leftPadding;
            int d_y0 = getMinY() + topPadding;

            int d_w = getWidth() - leftPadding - rightPadding;
            d_w = Math.max(d_w, 0);

            int d_h = getHeight() - topPadding - bottomPadding;
            d_h = Math.max(d_h, 0);

            theDest = new Rectangle(d_x0, d_y0, d_w, d_h);
        } else {
            theDest = getBounds();
        }
    }

    /**
     * Returns the number of pixels needed to the left of the central pixel.
     *
     * @return The left padding factor.
     */
    public int getLeftPadding() {
        return leftPadding;
    }

    /**
     * Returns the number of pixels needed to the right of the central pixel.
     *
     * @return The right padding factor.
     */
    public int getRightPadding() {
        return rightPadding;
    }

    /**
     * Returns the number of pixels needed above the central pixel.
     *
     * @return The top padding factor.
     */
    public int getTopPadding() {
        return topPadding;
    }

    /**
     * Returns the number of pixels needed below the central pixel.
     *
     * @return The bottom padding factor.
     */
    public int getBottomPadding() {
        return bottomPadding;
    }

    /**
     * Retrieve the <code>BorderExtender</code> object associated with this class instance. The object is returned by
     * reference.
     *
     * @return The associated <code>BorderExtender</code> object or <code>null</code>.
     * @since JAI 1.1
     */
    public BorderExtender getBorderExtender() {
        return extender;
    }

    /**
     * Returns a conservative estimate of the destination region that can potentially be affected by the pixels of a
     * rectangle of a given source. The resulting <code>Rectangle</code> is <u>not</u> clipped to the destination image
     * bounds.
     *
     * @param sourceRect the <code>Rectangle</code> in source coordinates.
     * @param sourceIndex the index of the source image.
     * @return a <code>Rectangle</code> indicating the potentially affected destination region, or <code>null</code> if
     *     the region is unknown.
     * @throws IllegalArgumentException if <code>sourceIndex</code> is negative or greater than the index of the last
     *     source.
     * @throws IllegalArgumentException if <code>sourceRect</code> is <code>null</code>.
     */
    public Rectangle mapSourceRect(Rectangle sourceRect, int sourceIndex) {

        if (sourceRect == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic1"));
        }

        int lpad = getLeftPadding();
        int rpad = getRightPadding();
        int tpad = getTopPadding();
        int bpad = getBottomPadding();

        return new Rectangle(
                sourceRect.x + lpad,
                sourceRect.y + tpad,
                sourceRect.width - lpad - rpad,
                sourceRect.height - tpad - bpad);
    }

    /**
     * Returns a conservative estimate of the region of a specified source that is required in order to compute the
     * pixels of a given destination rectangle. The resulting <code>Rectangle</code> is <u>not</u> clipped to the source
     * image bounds.
     *
     * @param destRect the <code>Rectangle</code> in destination coordinates.
     * @param sourceIndex the index of the source image.
     * @return a <code>Rectangle</code> indicating the required source region.
     * @throws IllegalArgumentException if <code>sourceIndex</code> is negative or greater than the index of the last
     *     source.
     * @throws IllegalArgumentException if <code>destRect</code> is <code>null</code>.
     */
    public Rectangle mapDestRect(Rectangle destRect, int sourceIndex) {
        if (destRect == null) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic0"));
        }

        if (sourceIndex < 0 || sourceIndex >= getNumSources()) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic1"));
        }

        int lpad = getLeftPadding();
        int rpad = getRightPadding();
        int tpad = getTopPadding();
        int bpad = getBottomPadding();

        return new Rectangle(
                destRect.x - lpad, destRect.y - tpad, destRect.width + lpad + rpad, destRect.height + tpad + bpad);
    }

    /**
     * Computes a tile. If source cobbling was requested at construction time, the source tile boundaries are overlayed
     * onto the destination, cobbling is performed for areas that intersect multiple source tiles, and <code>
     * computeRect(Raster[], WritableRaster, Rectangle)</code> is called for each of the resulting regions. Otherwise,
     * <code>computeRect(PlanarImage[], WritableRaster,
     * Rectangle)</code> is called once to compute the entire active area of the tile.
     *
     * <p>The image bounds may be larger than the bounds of the source image. In this case, samples for which there are
     * no no corresponding sources are set to zero.
     *
     * @param tileX The X index of the tile.
     * @param tileY The Y index of the tile.
     * @return The tile as a <code>Raster</code>.
     */
    public Raster computeTile(int tileX, int tileY) {
        if (!cobbleSources) {
            return super.computeTile(tileX, tileY);
        }

        /* Create a new WritableRaster to represent this tile. */
        Point org = new Point(tileXToX(tileX), tileYToY(tileY));
        WritableRaster dest = createWritableRaster(sampleModel, org);

        /* Clip output rectangle to image bounds. */
        Rectangle rect = new Rectangle(org.x, org.y, sampleModel.getWidth(), sampleModel.getHeight());

        Rectangle destRect = rect.intersection(theDest);
        if ((destRect.width <= 0) || (destRect.height <= 0)) {
            return dest;
        }

        /* account for padding in srcRectangle */
        PlanarImage s = getSource(0);
        // Fix 4639755: Area operations throw exception for
        // destination extending beyond source bounds
        // The default dest image area is the same as the source
        // image area.  However, when an ImageLayout hint is set,
        // this might be not true.  So the destRect should be the
        // intersection of the provided rectangle, the destination
        // bounds and the source bounds.
        destRect = destRect.intersection(s.getBounds());
        Rectangle srcRect = new Rectangle(destRect);
        srcRect.x -= getLeftPadding();
        srcRect.width += getLeftPadding() + getRightPadding();
        srcRect.y -= getTopPadding();
        srcRect.height += getTopPadding() + getBottomPadding();

        /*
         * The tileWidth and tileHeight of the source image
         * may differ from this tileWidth and tileHeight.
         */
        IntegerSequence srcXSplits = new IntegerSequence();
        IntegerSequence srcYSplits = new IntegerSequence();

        // there is only one source for an AreaOpImage
        s.getSplits(srcXSplits, srcYSplits, srcRect);

        // Initialize new sequences of X splits.
        IntegerSequence xSplits = new IntegerSequence(destRect.x, destRect.x + destRect.width);

        xSplits.insert(destRect.x);
        xSplits.insert(destRect.x + destRect.width);

        srcXSplits.startEnumeration();
        while (srcXSplits.hasMoreElements()) {
            int xsplit = srcXSplits.nextElement();
            int lsplit = xsplit - getLeftPadding();
            int rsplit = xsplit + getRightPadding();
            xSplits.insert(lsplit);
            xSplits.insert(rsplit);
        }

        // Initialize new sequences of Y splits.
        IntegerSequence ySplits = new IntegerSequence(destRect.y, destRect.y + destRect.height);

        ySplits.insert(destRect.y);
        ySplits.insert(destRect.y + destRect.height);

        srcYSplits.startEnumeration();
        while (srcYSplits.hasMoreElements()) {
            int ysplit = srcYSplits.nextElement();
            int tsplit = ysplit - getBottomPadding();
            int bsplit = ysplit + getTopPadding();
            ySplits.insert(tsplit);
            ySplits.insert(bsplit);
        }

        /*
         * Divide destRect into sub rectangles based on the source splits,
         * and compute each sub rectangle separately.
         */
        int x1, x2, y1, y2;
        Raster[] sources = new Raster[1];

        ySplits.startEnumeration();
        for (y1 = ySplits.nextElement(); ySplits.hasMoreElements(); y1 = y2) {
            y2 = ySplits.nextElement();

            int h = y2 - y1;
            int py1 = y1 - getTopPadding();
            int py2 = y2 + getBottomPadding();
            int ph = py2 - py1;

            xSplits.startEnumeration();
            for (x1 = xSplits.nextElement(); xSplits.hasMoreElements(); x1 = x2) {
                x2 = xSplits.nextElement();

                int w = x2 - x1;
                int px1 = x1 - getLeftPadding();
                int px2 = x2 + getRightPadding();
                int pw = px2 - px1;

                // Fetch the padded src rectangle
                Rectangle srcSubRect = new Rectangle(px1, py1, pw, ph);
                sources[0] = (extender != null) ? s.getExtendedData(srcSubRect, extender) : s.getData(srcSubRect);

                // Make a destRectangle
                Rectangle dstSubRect = new Rectangle(x1, y1, w, h);
                computeRect(sources, dest, dstSubRect);

                // Recycle the source tile
                if (s.overlapsMultipleTiles(srcSubRect)) {
                    recycleTile(sources[0]);
                }
            }
        }
        return dest;
    }
}
